package tyr

import (
	"bytes"
	"crypto/sha256"
	"database/sql"
	"embed"
	"fmt"
	"html/template"
	"io"
	"log"
	"net/http"
	"strings"
	"time"

	"apiote.xyz/p/asgard/himinbjorg"
	"apiote.xyz/p/asgard/idavollr"
	"apiote.xyz/p/asgard/jotunheim"

	"github.com/emersion/go-imap"
	"github.com/emersion/go-imap/client"
)

func addSentTo(db *sql.DB, c *client.Client, mbox *imap.MailboxStatus, config jotunheim.Config) error {
	from := uint32(1)
	to := mbox.Messages
	seqset := new(imap.SeqSet)
	seqset.AddRange(from, to)

	messages := make(chan *imap.Message, 10)
	done := make(chan error, 1)
	go func() {
		done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope}, messages)
	}()

	for msg := range messages {
		recipients := append(msg.Envelope.To, msg.Envelope.Cc...)
		sender := msg.Envelope.From[0].Address()
		for _, recipient := range recipients {
			knownAddress := himinbjorg.KnownAddress{
				AddressFrom: recipient.Address(),
				AddressTo:   sender,
				Ban:         false,
			}
			err := himinbjorg.InsertKnownAddress(db, knownAddress)
			if err != nil {
				log.Println(err)
				continue
			}
		}
	}

	clearLocks(db, config)

	if err := <-done; err != nil {
		return err
	}
	return nil
}

func clearLocks(db *sql.DB, config jotunheim.Config) {
	locks, _ := himinbjorg.ListLocks(db)
	for _, lock := range locks {
		knownAddresses, _ := himinbjorg.GetKnownAddress(db, lock.Address)
		for _, knownAddress := range knownAddresses {
			if knownAddress.AddressTo == lock.Recipient {
				releaseQuarantine(db, config, lock, config.Tyr.ImapFolderInbox)
			}
		}
	}
}

func listInboxes(c *client.Client, config jotunheim.Config) ([]*imap.MailboxInfo, error) {
	mailboxes := make(chan *imap.MailboxInfo, 10)
	inboxes := []*imap.MailboxInfo{}
	done := make(chan error, 1)
	go func() {
		done <- c.List("", "*", mailboxes)
	}()

	for m := range mailboxes {
		if m.Name != config.Tyr.ImapFolderArchive && m.Name != config.Tyr.ImapFolderDrafts && m.Name != config.Tyr.ImapFolderJunk &&
			m.Name != config.Tyr.ImapFolderQuarantine && m.Name != config.Tyr.ImapFolderSent && m.Name != config.Tyr.ImapFolderTrash {
			inboxes = append(inboxes, m)
		}
	}

	if err := <-done; err != nil {
		return inboxes, err
	}
	return inboxes, nil
}

func checkInbox(db *sql.DB, config jotunheim.Config, c *client.Client, mbox *imap.MailboxStatus) error {
	from := uint32(1)
	to := mbox.Messages
	seqset := new(imap.SeqSet)
	moveSet := new(imap.SeqSet)
	seqset.AddRange(from, to)

	messages := make(chan *imap.Message, 10)
	done := make(chan error, 1)
	go func() {
		done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid}, messages)
	}()

messagesLoop:
	for msg := range messages {
		for _, flag := range msg.Flags {
			if flag == imap.FlaggedFlag {
				log.Printf("Ignoring %s as flagged\n", msg.Envelope.Subject)
				continue messagesLoop
			}
		}
		recipients := append(msg.Envelope.To, msg.Envelope.Cc...)
		recipients = append(recipients, msg.Envelope.Bcc...)
		domainRecipient := findDomainRecipient(recipients, config)
		recipients_ := map[string]struct{}{}
		for _, recipient := range recipients {
			recipients_[recipient.Address()] = struct{}{}
		}
		sender := msg.Envelope.From[0]

		senderRows, err := himinbjorg.GetKnownAddress(db, sender.Address())
		if err != nil {
			log.Println(err)
			continue
		}
		lock, err := himinbjorg.GetAddressLock(db, sender.Address())
		if err != nil {
			log.Println(err)
			continue
		}

		for _, senderRow := range senderRows {
			if _, present := recipients_[senderRow.AddressTo]; present || senderRow.AddressTo == "*" {
				if senderRow.Ban {
					idavollr.MoveMsg(c, msg, config.Tyr.ImapFolderJunk)
					log.Printf("%s -> %s is a known offender\n", senderRow.AddressFrom, senderRow.AddressTo)
				} else {
					log.Printf("%s -> %s is a known friend\n", senderRow.AddressFrom, senderRow.AddressTo)
				}
				continue messagesLoop
			}
		}

		if !lock.Empty() {
			now := time.Now()
			weekBefore := now.AddDate(0, 0, -7)
			if lock.Date.Before(weekBefore) {
				if domainRecipient == config.Tyr.MainEmailAddress {
					idavollr.SendRepeatedQuarantine(sender, lock)
				}
				lock.Date = time.Now()
				himinbjorg.UpdateLock(db, lock)
			} else {
				log.Printf("lock %+v is still valid\n", lock)
			}
			log.Printf("moving repeated %v : %s from %s to quarantine\n", msg.Envelope.Date, msg.Envelope.Subject, msg.Envelope.From[0].Address())
			moveSet.AddNum(msg.Uid)
			continue
		}

		if domainRecipient == config.Tyr.MainEmailAddress {
			idavollr.SendQuarantine(sender)
		}
		lock = himinbjorg.NewLock(sender.Address(), domainRecipient)
		himinbjorg.InsertLock(db, lock)
		log.Printf("moving %v : %s from %s to %s to quarantine\n", msg.Envelope.Date, msg.Envelope.Subject, msg.Envelope.From[0].Address(), domainRecipient)
		moveSet.AddNum(msg.Uid)
	}

	if err := <-done; err != nil {
		return err
	}
	return idavollr.MoveMultiple(c, moveSet, config.Tyr.ImapFolderQuarantine)
}

func findDomainRecipient(recipients []*imap.Address, config jotunheim.Config) string {
	for _, recipient := range recipients {
		if recipient.HostName == config.Tyr.RecipientDomain {
			return recipient.Address()
		}
	}
	return config.Tyr.MainEmailAddress
}

func Tyr(db *sql.DB, config jotunheim.Config) {
	c, err := client.DialTLS(config.Tyr.ImapAddress, nil)
	if err != nil {
		log.Fatalln(err)
	}
	log.Println("Connected")
	defer c.Logout()
	if err := c.Login(config.Tyr.ImapUsername, config.Tyr.ImapPassword); err != nil {
		log.Fatalln(err)
	}
	log.Println("Logged in")

	mbox, err := c.Select(config.Tyr.ImapFolderSent, false)
	if err != nil {
		log.Fatalln(err)
	}
	err = addSentTo(db, c, mbox, config)
	if err != nil {
		log.Fatalln(err)
	}

	inboxes, err := listInboxes(c, config)
	if err != nil {
		log.Fatalln(err)
	}
inboxLoop:
	for _, inbox := range inboxes {
		for _, attribute := range inbox.Attributes {
			if attribute == "\\Noselect" {
				continue inboxLoop
			}
		}
		mbox, err := c.Select(inbox.Name, false)
		if err != nil {
			log.Fatalln(err)
		}
		checkInbox(db, config, c, mbox)
	}
}

func ListLocks(db *sql.DB) {
	locks, err := himinbjorg.ListLocks(db)
	if err != nil {
		log.Fatalln(err)
	}
	if len(locks) == 0 {
		fmt.Println("no locks")
	}
	for _, lock := range locks {
		fmt.Printf("%s: %s\n", lock.Token, lock.Address)
	}
}

func Release(db *sql.DB, config jotunheim.Config, token, addressTo, dest string) {
	lock, err := himinbjorg.GetLock(db, token)
	if err != nil {
		log.Fatalln(err)
	}
	if addressTo != "" {
		lock.Recipient = addressTo
	}
	err = releaseQuarantine(db, config, lock, dest)
	if err != nil {
		log.Fatalln(err)
	}
}

type TyrData struct {
	Address string
	Token   string
	Captcha string
	Error   string
}

func releaseQuarantine(db *sql.DB, config jotunheim.Config, lock himinbjorg.Lock, dest string) error {
	himinbjorg.DeleteLock(db, lock)
	knownAddress := himinbjorg.KnownAddress{
		AddressFrom: lock.Address,
		AddressTo:   lock.Recipient,
		Ban:         dest == config.Tyr.ImapFolderJunk,
	}
	err := himinbjorg.InsertKnownAddress(db, knownAddress)
	if err != nil {
		return err
	}
	c, err := client.DialTLS(config.Tyr.ImapAddress, nil)
	if err != nil {
		return err
	}
	defer c.Logout()
	if err := c.Login(config.Tyr.ImapUsername, config.Tyr.ImapPassword); err != nil {
		log.Fatalln(err)
	}
	mbox, err := c.Select(config.Tyr.ImapFolderQuarantine, false)
	if err != nil {
		log.Fatalln(err)
	}
	idavollr.MoveFromQuarantine(c, mbox, lock.Address, dest)
	return nil
}

func Serve(db *sql.DB, config jotunheim.Config, templatesFs embed.FS) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		r.ParseForm()
		formAddress := r.Form.Get("address")
		formToken := r.Form.Get("token")
		formError := r.Form.Get("error")
		if r.Method == "GET" {
			tyrData := TyrData{
				Address: formAddress,
				Token:   formToken,
				Captcha: "$696a04444feea781aeca9c546e220e0981aff4a8db0b2998decdf13265a95c31", // todo with salt and randomised time
				Error:   formError,
			}
			t, _ := template.ParseFS(templatesFs, "templates/tyr.html")
			b := bytes.NewBuffer([]byte{})
			_ = t.Execute(b, tyrData)
			io.Copy(w, b)
		} else if r.Method == "POST" {
			formCaptcha := r.Form.Get("captcha")
			captchaResult := strings.Split(r.Form.Get("captcha_result"), "$")
			shaCaptcha := sha256.Sum256([]byte(formCaptcha + captchaResult[0]))
			hexCaptcha := fmt.Sprintf("%x", shaCaptcha)
			if hexCaptcha != captchaResult[1] {
				w.Header().Add("Location", "?error=captcha")
				w.WriteHeader(303)
				return
			}

			lock, err := himinbjorg.GetAddressLock(db, "*")
			if err != nil {
				w.WriteHeader(500)
				w.Write([]byte(err.Error()))
				return
			}
			if lock.Token != "" && lock.Token == formToken {
				lock.Address = formAddress
				err = releaseQuarantine(db, config, lock, config.Tyr.ImapFolderInbox)
				if err != nil {
					w.WriteHeader(500)
					w.Write([]byte(err.Error()))
				} else {
					w.Header().Add("Location", "?error=success")
					w.WriteHeader(303)
				}
				return
			}

			lock, err = himinbjorg.GetAddressLock(db, formAddress)
			if err != nil {
				w.WriteHeader(500)
				w.Write([]byte(err.Error()))
				return
			}
			if lock.Token == "" {
				w.Header().Add("Location", "?error=address&address="+formAddress)
				w.WriteHeader(303)
				return
			}
			if lock.Token != formToken {
				w.Header().Add("Location", "?error=token&address="+formAddress)
				w.WriteHeader(303)
				return
			}
			err = releaseQuarantine(db, config, lock, config.Tyr.ImapFolderInbox)
			if err != nil {
				w.WriteHeader(500)
				w.Write([]byte(err.Error()))
			} else {
				w.Header().Add("Location", "?error=success")
				w.WriteHeader(303)
			}
			return
		} else {
			w.WriteHeader(405)
		}
	}
}
